/**
 * Copyright (c) 2011 Technical Centre Radio Systems Ltd.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package ua.tcrs.tabula.container;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Field;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.List;
import java.util.Arrays;
import java.io.IOException;

import ua.tcrs.tabula.config.Configuration;
import ua.tcrs.tabula.config.SavableConfiguration;
import ua.tcrs.tabula.config.NullConfiguration;
import ua.tcrs.tabula.log.Logger;
import ua.tcrs.tabula.log.NopLogger;
import ua.tcrs.tabula.util.Iterables;
import ua.tcrs.tabula.util.Reflect;
import ua.tcrs.tabula.InjectService;
import ua.tcrs.tabula.Subscription;
import ua.tcrs.tabula.Event;
import ua.tcrs.tabula.Dispatcher;
import ua.tcrs.tabula.Listener;
import ua.tcrs.tabula.Topic;
import ua.tcrs.tabula.TabulaContext;
import ua.tcrs.tabula.Tabula;

/**
 * Base Container
 */

public class BaseContainer implements TabulaContext, Container {
	ServiceRegistry registry;
	public BaseContainer( Iterable<Object> modules, Logger logger, ServiceRegistry serviceRegistry ) {
		this.logger = logger;
		this.registry = serviceRegistry;
		this.dispatcher = new SimpleDispatcher();
		initContainerServices();
		for ( Object module: modules ) {
			injectServices( module );
			initializeModule( module );
		}
		dispatcher.start();
		dispatcher.publish( new Event<Object,Object>( "tabula", "inited" ) );
		logger.trace( "Container inited" );
	}
	public BaseContainer( Iterable<Object> modules ) {
		this( modules, new NopLogger(), new BaseServiceRegistry() );
	}
	public BaseContainer( Object... modules ) {
		this( Arrays.asList( modules ) );
	}
	protected Logger logger;
	protected SimpleDispatcher dispatcher;
	protected void initContainerServices() {
		registry.bindService( logger, Logger.class );
		registry.bindService( this, TabulaContext.class );
		registry.bindService( dispatcher, Dispatcher.class );
		// Try to inject TabulaContext before initialize modules
		Tabula.setTabulaContext( this );
	}
	private void initializeModule( Object modInstance ) {
		List<Subscription> subscpriptions = dispatcher.subscribeAnnotated( modInstance );
		logger.trace( "Subscribe ", subscpriptions.size(), " annotated listeners in \"", modInstance.getClass(), "\"" );
		for ( Method m: modInstance.getClass().getDeclaredMethods() ) {
			// TODO: equals("init") -> startsWith("init")
			if ( m.getName().equals("init") ) {
				Object srvo = injectServicesIntoMethod( modInstance, m );
				logger.trace( "Invoked ", m );
				if ( srvo != null ) {
					if ( srvo instanceof Iterable ) {
						@SuppressWarnings("unchecked")
						Iterable<Object> isrvo = (Iterable<Object>)srvo;
						for ( Object iisrvo: isrvo ) registerService( iisrvo, m.toString() );
					} else {
						registerService( srvo, m.toString() );
					}
				}
				break;
			}
		}
		
	}
	private void registerService( Object service, String creatorName ) {
		dispatcher.subscribeAnnotated( service );
		registry.bindService( service );
		logger.trace( "Registered service \"",service.getClass(),"\" returned by \"",creatorName,"\"" );
	}
	private void injectServices( Object modInstance ) {
		for ( Field f: Reflect.allAnnotatedFields( modInstance.getClass(), InjectService.class ) ) {
			logger.trace( "Field \"", f,"\" annotated by InjectService" );
			injectServicesIntoField( modInstance, f );
		}
		for ( Method m: Reflect.allAnnotatedMethods( modInstance.getClass(), InjectService.class ) ) {
			logger.trace( "Method \"", m,"\" annotated by InjectService" );
			injectServicesIntoMethod( modInstance, m );
		}
	}
	private Object injectServicesIntoField( Object modInstance, Field field ) {
		Object service = registry.getService( field.getType() );
		if ( service == null ) {
			throw new RuntimeException( "No service \""+field.getType()+"\" required by \""+field+"\"" );
		}
		field.setAccessible(true);
		try {
			field.set( modInstance, service );
			logger.trace( "Service \"", service, "\" injected into \"", field, "\"" );
			return service;
		} catch ( IllegalAccessException e ) {
			throw new RuntimeException( e );
		}
	}
	private Object injectServicesIntoMethod( Object modInstance, Method method ) {
		Class<?>[] paramTypes = method.getParameterTypes();
		if ( paramTypes.length == 0 && !method.getName().startsWith("init") ) {
			throw new RuntimeException( "Method \""+method+"\" anotated by InjectService has no parameters" );
		}
		Object[] services = new Object[paramTypes.length];
		for ( int i=0; i<paramTypes.length; i++ ) {
			services[i] = registry.getService( paramTypes[i] );
			if ( services[i] == null ) {
				throw new RuntimeException( "No service \""+paramTypes[i]+"\" required by \""+method+"\"" );
			}
		}
		method.setAccessible(true);
		try {
			Object r = method.invoke( modInstance, services );
			// TODO: another message for "init" method
			logger.trace( "Services \"", Arrays.asList( services ), "\" injected into \"", method, "\"" );
			return r;
		} catch ( IllegalAccessException e ) {
			throw new RuntimeException( e );
		} catch ( InvocationTargetException e ) {
			throw new RuntimeException( e );
		}
	}
	public Logger getLogger() {
		return logger;
	}
	public Configuration getConfig() {
		if ( registry.containsService( Configuration.class ) ) {
			return registry.getService( Configuration.class );
		} else {
			return new NullConfiguration();
		}
	}
	public Dispatcher getDispatcher() {
		return dispatcher;
	}
	public boolean containsService( Class<?> iface ) {
		return registry.containsService( iface );
	}
	public <T> T getService( Class<T> iface ) {
		return registry.getService( iface );
	}
	
	private BlockingQueue<Integer> closeQueue = new SynchronousQueue<Integer>();
	public int run() {
		dispatcher.subscribe( new Topic<Object>( "tabula", "close" ), new Listener<Object,Integer>() {
			public Object onEvent( Event<Object,Integer> ev ) {
				close( (ev.getBody() != null && ev.getBody() instanceof Integer)?(Integer)ev.getBody():0 );
				return null;
			}
		} );
		
		// start
		new Thread() {
			public void run() {
				try {
					// TODO: must stop publishing if close invoked
					List<Object> answers = dispatcher.publish( new Event<Object,Object>( "tabula", "start" ) );
					if ( answers.size() > 0 ) {
						for ( Object answer: answers ) {
							logger.error( "Start publish answer: ", answer );
							// System.out.println( answer );
							// TODO
						}
						close(-1);
					}
					logger.trace( "Start published succesfully" );
					//dispatcher.publish( new Event<Object,Object>( "tabula", "started" ) );
				} catch ( RuntimeException e ) {
					logger.trace( "Start publishing exception", e );
					close(-1);
				}
				
				// TODO: do close if no active threads
			}
		}.start();
		int exitCode;
		for (;;) {
			logger.trace( "Awainitg for close" );
			// awaiting for close
			try {
				exitCode = closeQueue.take();
				// ask for close
				// TODO: catch RuntimeException
				if ( exitCode >= 0 ) {
					if ( Iterables.all( Iterables.filter( dispatcher.publish( new Event<Object,Object>( "tabula", "canClose" ) ), Boolean.class ) ) ) break;
				} else {
					break;
				}
			} catch ( InterruptedException e ) {
				// TODO
			}
		}
		// do close
		dispatcher.publish( new Event<Object,Object>( TABULA_EVENT_GROUP, "doClose" ) );
		Configuration conf = getConfig();
		if ( conf instanceof SavableConfiguration ) {
			try {
				((SavableConfiguration)conf).save();
			} catch ( IOException e ) {
				logger.trace( "Failed to save configuration" );
			}
		}
		// TODO: waiting all child threads (is this possible?)
		return exitCode;
	}
	public void close( int code ) {
		try {
			closeQueue.put( code );
			Thread.currentThread().interrupt();
		} catch ( InterruptedException e ) {
			// TODO
		}
	}
	public static final String TABULA_EVENT_GROUP = "tabula";
}

